/* Copyright (C) 2007-2010 STS Soft

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; version 2 of the License.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA */

using System;
using System.IO;
using System.Linq;

namespace STSdb.Files
{
    /// <summary>
    /// Class is ThreadSafe
    /// </summary>
    public class RawFile : IFile
    {
        private ulong length;

        internal int Depth;
        internal int References = 0;
        internal ulong RootSectorID { get; private set; }

        public readonly object SyncRoot = new object();
        public readonly FileSystem FileSystem;
        public readonly ulong Handle;

        /// <summary>
        /// Create new RawFile
        /// </summary>
        internal RawFile(FileSystem fileSystem, ulong handle)
            : this(fileSystem, handle, fileSystem.GetFreeSectorID(), 0)
        {
            //optimization, nothing serious
            Node rootNode = new Node(this, RootSectorID);
            lock (FileSystem.Cache)
                FileSystem.Cache.Packet(rootNode.SectorID, rootNode);
        }

        /// <summary>
        /// Open existing RawFile
        /// </summary>
        internal RawFile(FileSystem fileSystem, ulong handle, ulong rootSectorID, ulong length)
        {
            if (fileSystem == null)
                throw new ArgumentNullException("fileSystem");

            FileSystem = fileSystem;
            Handle = handle;
            RootSectorID = rootSectorID;
            Length = length;
            Depth = GetDepth(Length);
        }

        internal Node Root
        {
            get
            {
                Node rootNode;
                lock (FileSystem.Cache)
                    rootNode = FileSystem.Cache.Retrieve(RootSectorID);

                if (rootNode != null)
                    return rootNode;

                rootNode = new Node(this, RootSectorID);
                rootNode.Load();
                lock (FileSystem.Cache)
                    FileSystem.Cache.Packet(rootNode.SectorID, rootNode);

                return rootNode;
            }
        }

        internal static int GetDepth(ulong length)
        {
            ulong sectorCount = (ulong)Math.Ceiling((double)(length / Sector.SIZE_64));
            return sectorCount <= 1 ? 1 : (int)Math.Ceiling(Math.Log(sectorCount, Node.BRANCH_LENGTH));
        }

        /// <summary>
        /// Returns the path. The first element is the index in the leaf, the last element is the index in the root
        /// </summary>
        private ushort[] Decompose(ulong number, ushort newBase)
        {
            ushort[] array = new ushort[Node.MAX_DEPTH];

            int index = 0;
            while (number > 0)
            {
                array[index] = (ushort)(number % newBase);
                index++;
                number /= newBase;
            }

            if (index < Depth)
                index = Depth;

            Array.Resize<ushort>(ref array, index);
            return array;
        }

        private void Next(ref ushort[] path)
        {
            int i = 0;
            for (; i < path.Length; i++)
            {
                if (path[i] < Node.BRANCH_LENGTH - 1)
                {
                    path[i]++;
                    break;
                }

                path[i] = 0;
            }

            if (i == path.Length)
            {
                Array.Resize(ref path, path.Length + 1);
                path[i]++;
            }
        }

        public void Flush()
        {
            lock (SyncRoot)
            {
                if (Handle <= FileSystem.MAX_HANDLE)
                {
                    FileSystem.SetFileInfo(Handle, new FileSystem.FileInfo(RootSectorID, Length));

                    if (this != FileSystem.RecycleFile)
                        FileSystem.Flush();
                }

                Node[] nodes;
                lock (FileSystem.Cache)
                    nodes = FileSystem.Cache.Where(x => x.Value.RawFile == this && x.Value.IsModified).Select(x => x.Value).ToArray();

                foreach (var node in nodes)
                    node.Save();
            }

            FileSystem.Volume.Flush();
        }

        public void Close()
        {
            lock (SyncRoot)
            {
                Flush();
                FileSystem.CloseFile(this);

                lock (FileSystem.Cache)
                {
                    var nodes = FileSystem.Cache.Where(x => x.Value.RawFile == this).Select(x => x.Value).ToArray();
                    foreach (var node in nodes)
                        FileSystem.Cache.Exclude(node.SectorID);
                }
            }
        }

        #region Writers

        private void InternalWrite(ushort[] path, ulong offset, byte[] buffer, int index, int count)
        {
            //update root
            for (int i = Depth; i < path.Length; i++)
            {
                Node newRoot = new Node(this, FileSystem.GetFreeSectorID());
                newRoot.SetPointer(0, RootSectorID);
                RootSectorID = newRoot.SectorID;

                lock (FileSystem.Cache)
                    FileSystem.Cache.Packet(newRoot.SectorID, newRoot);
            }

            Depth = path.Length;

            Node node = Root;
            for (int i = path.Length - 1; i >= 1; i--)
                node = node[path[i], true];

            ulong sectorID = node.GetPointer(path[0]);
            if (sectorID == Node.INVALID_SECTOR_ID)
            {
                sectorID = FileSystem.GetFreeSectorID();
                node.SetPointer(path[0], sectorID);
            }

            ulong position = sectorID * Sector.SIZE_64 + offset;
            FileSystem.Volume.Write(position, buffer, index, count);
        }

        public void Write(ulong position, byte[] buffer, int index, int count)
        {
            lock (SyncRoot)
            {
                ulong length = Math.Max(Length, position + (ulong)count);

                ushort[] path = Decompose(position / Sector.SIZE_64, Node.BRANCH_LENGTH);
                ulong offset = position % Sector.SIZE_64;

                while (count > 0)
                {
                    int chunk = Math.Min((int)(Sector.SIZE_64 - offset), count);
                    InternalWrite(path, offset, buffer, index, chunk);
                    offset = 0;

                    index += chunk;
                    count -= chunk;
                    Next(ref path);
                }

                Length = length;
            }
        }

        #endregion

        #region Readers

        /// <summary>
        /// read within one sector
        /// </summary>
        private void InternalRead(ushort[] path, ulong offset, byte[] buffer, int index, int count)
        {
            if (Depth < path.Length)
            {
                for (int j = 0; j < count; j++)
                    buffer[index + j] = 0;

                return;
            }

            Node node = Root;
            for (int i = path.Length - 1; i >= 1; i--)
            {
                node = node[path[i], false];
                if (node == null)
                {
                    for (int j = 0; j < count; j++)
                        buffer[index + j] = 0;

                    return;
                }
            }

            ulong sectorID = node.GetPointer(path[0]);
            if (sectorID == Node.INVALID_SECTOR_ID)
            {
                for (int j = 0; j < count; j++)
                    buffer[index + j] = 0;

                return;
            }

            ulong position = sectorID * Sector.SIZE_64 + offset;
            FileSystem.Volume.Read(position, buffer, index, count);
        }

        public void Read(ulong position, byte[] buffer, int index, int count)
        {
            lock (SyncRoot)
            {
                ulong positionPlusCount = position + (ulong)count;
                if (positionPlusCount > Length)
                {
                    ulong delta = Math.Min(positionPlusCount - Length, (ulong)count);
                    for (int i = buffer.Length - (int)delta; i < buffer.Length; i++)
                        buffer[i] = 0;

                    count -= (int)delta;
                }

                ushort[] path = Decompose(position / Sector.SIZE_64, Node.BRANCH_LENGTH);
                ulong offset = position % Sector.SIZE_64;

                while (count > 0)
                {
                    int chunk = Math.Min((int)(Sector.SIZE_64 - offset), count);
                    InternalRead(path, offset, buffer, index, chunk);
                    offset = 0;

                    count -= chunk;
                    index += chunk;
                    Next(ref path);
                }
            }
        }

        #endregion

        public ulong Length
        {
            get
            {
                lock (SyncRoot)
                    return length;
            }
            private set
            {
                lock (SyncRoot)
                    length = value;
            }
        }

        #region Read/Write Specialized for internal use

        public MemoryStream InternalRead(Ptr ptr)
        {
            if (ptr.IsNull)
                return new MemoryStream(new byte[0], 0, 0, false, true);

            byte[] buffer = new byte[ptr.Size];
            Read(ptr.Position, buffer, 0, buffer.Length);

            return new MemoryStream(buffer, 0, buffer.Length, true, true);
        }

        public Ptr InternalWrite(Stream stream)
        {
            if (stream.Length == 0)
                return Ptr.NULL;

            Ptr ptr = new Ptr(Length, (ulong)stream.Length);
            byte[] buffer = new byte[stream.Length];
            stream.Seek(0, SeekOrigin.Begin);
            stream.Read(buffer, 0, buffer.Length);
            Write(Length, buffer, 0, buffer.Length);

            return ptr;
        }

        #endregion
    }
}
